[Flutter] providerでのcontextの読み取り方法3種類を使い比べてみる
こんにちは、CX事業本部 IoT事業部の若槻です。
以前のエントリでFlutterの状態管理ライブラリproviderを使用して、あるWidgetでの値の変更を別のWidgetに伝搬させる実装を行いました。
providerでは、状態として管理している値(context)を読み取る方法は次の3種類が提供さています。
context.watch<T>()
context.read<T>()
context.select<T, R>(R cb(T value))
今回は、providerでのcontextの読み取り方法3種類を使い比べてみました。
やってみた
context.watch
context.watch
を使うと、プロバイダーの値の更新を監視して、更新時にWidgetに反映することができます。
以前のエントリでFlutterの状態管理ライブラリproviderで作成したコードほぼそのままですが、context.watch
でCityProvider
の更新を読み取るようにしています。
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CityProvider extends ChangeNotifier { City value = City.ueno; void changeCity(City newValue) { value = newValue; notifyListeners(); } } void main() => runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => CityProvider()), ], child: const MyApp(), ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return MaterialApp( title: _title, home: Scaffold( appBar: AppBar(title: const Text(_title)), body: const Center( child: MyStatefulWidget(), ), ), ); } } enum City { ueno('上野'), shinjuku('新宿'), akihabara('秋葉原'), ikebukuro('池袋'), shibuya('渋谷'); final String displayName; const City(this.displayName); } class MyStatefulWidget extends StatefulWidget { const MyStatefulWidget({super.key}); @override State<MyStatefulWidget> createState() => _MyStatefulWidgetState(); } class _MyStatefulWidgetState extends State<MyStatefulWidget> { @override Widget build(BuildContext context) { return Column( children: const <Widget>[ RadioWidget(radioValue: City.ueno), RadioWidget(radioValue: City.shinjuku), RadioWidget(radioValue: City.akihabara), RadioWidget(radioValue: City.ikebukuro), RadioWidget(radioValue: City.shibuya), SizedBox( height: 30, ), SelectedCityText() ], ); } } class RadioWidget extends StatefulWidget { final City radioValue; const RadioWidget({super.key, required this.radioValue}); @override State<RadioWidget> createState() => _RadioWidget(); } class _RadioWidget extends State<RadioWidget> { @override Widget build(BuildContext context) { CityProvider cityProvider = context.watch<CityProvider>(); // return ListTile( title: Text(widget.radioValue.displayName), leading: Radio<City>( value: widget.radioValue, groupValue: cityProvider.value, onChanged: (City? value) { setState(() { cityProvider.changeCity(value!); }); }, ), ); } } class SelectedCityText extends StatelessWidget { const SelectedCityText({super.key}); @override Widget build(BuildContext context) { CityProvider cityProvider = context.watch<CityProvider>(); return Text('現在は ${cityProvider.value.displayName} が選択されています'); } }
context.read
context.read
を使うと、プロバイダーの値をWidgetの初回レンダリング時にのみ読み取ってWidgetに反映させることができます。
先程のコードでcontext.watch
をcontext.read
に置き換えます。
class _RadioWidget extends State<RadioWidget> { @override Widget build(BuildContext context) { CityProvider cityProvider = context.read<CityProvider>(); return ListTile( title: Text(widget.radioValue.displayName), leading: Radio<City>( value: widget.radioValue, groupValue: cityProvider.value, onChanged: (City? value) { setState(() { cityProvider.changeCity(value!); }); }, ), ); } } class SelectedCityText extends StatelessWidget { const SelectedCityText({super.key}); @override Widget build(BuildContext context) { CityProvider cityProvider = context.read<CityProvider>(); return Text('現在は ${cityProvider.value.displayName} が選択されています'); } }
するとラジオボタンを操作してCityProvider
を更新しても、他(および自身)のWidgetに値の更新が反映されなくなりました。
context.select
context.select
を使うと、プロバイダーで管理している一部値のみを監視して、更新時にWidgetに反映することができます。
context.selectを使わない場合
まずcontext.select
を使わずにcontext.watch
をそのまま使い続けた場合です。
コードを次のように修正します。CityProvider
に2つ目の値isChecked
を追加し、isChecked
を更新するWidget_CheckBox
を追加しています。
class CityProvider extends ChangeNotifier { City value = City.ueno; void changeCity(City newValue) { value = newValue; notifyListeners(); } bool isChecked = false; void changeIsChecked(bool newValue) { isChecked = newValue; notifyListeners(); } }
class _MyStatefulWidgetState extends State<MyStatefulWidget> { @override Widget build(BuildContext context) { return Column( children: const <Widget>[ RadioWidget(radioValue: City.ueno), RadioWidget(radioValue: City.shinjuku), RadioWidget(radioValue: City.akihabara), RadioWidget(radioValue: City.ikebukuro), RadioWidget(radioValue: City.shibuya), SizedBox( height: 30, ), SelectedCityText(), SizedBox( height: 30, ), CheckBox() ], ); } }
class SelectedCityText extends StatelessWidget { const SelectedCityText({super.key}); @override Widget build(BuildContext context) { CityProvider cityProvider = context.watch<CityProvider>(); print('_RadioWidget has rendered.'); //ビルドされたことをデバッグコンソールに表示 return Text('現在は ${cityProvider.value.displayName} が選択されています'); } } class CheckBox extends StatefulWidget { const CheckBox({super.key}); @override State<CheckBox> createState() => _CheckBox(); } class _CheckBox extends State<CheckBox> { @override Widget build(BuildContext context) { CityProvider cityProvider = context.watch<CityProvider>(); print('_CheckBox has rendered.'); //ビルドされたことをデバッグコンソールに表示 return CheckboxListTile( title: const Text('チェックボックス'), value: cityProvider.isChecked, onChanged: (bool? value) { setState(() { cityProvider.changeIsChecked(value!); }); }, ); } }
ここでCityProvider
の管理している値のうち、_RadioWidget
はvalue
のみ、_CheckBox
はisChecked
のみを使用しています。
しかしデバッグコンソールを見ながらWidgetを操作してCityProvider
の値を変更すると、それぞれのWidgetを操作する毎に、すべてのWidgetで再ビルドが走っていますね。
しかしこれだと無駄なビルドが行われてしまっているので改善の余地がありそうです。
context.selectを使った場合
次にcontext.select
を使用します。CityProvider
で管理している値はcontext.select((CityProvider cityProvider) => cityProvider.値)
のようにして監視が必要な値のみ読み取るようにし、またCityProvider
のメソッドはcontext.read<CityProvider>()
によりWidget内で使用できるようにします。
class _RadioWidget extends State<RadioWidget> { @override Widget build(BuildContext context) { CityProvider cityProvider = context.read<CityProvider>(); final value = context.select((CityProvider cityProvider) => cityProvider.value); return ListTile( title: Text(widget.radioValue.displayName), leading: Radio<City>( value: widget.radioValue, groupValue: value, onChanged: (City? value) { setState(() { cityProvider.changeCity(value!); }); }, ), ); } } class SelectedCityText extends StatelessWidget { const SelectedCityText({super.key}); @override Widget build(BuildContext context) { final value = context.select((CityProvider cityProvider) => cityProvider.value); print('_RadioWidget has rendered.'); //レンダリングされたことをDebugコンソールに表示 return Text('現在は ${value.displayName} が選択されています'); } } class CheckBox extends StatefulWidget { const CheckBox({super.key}); @override State<CheckBox> createState() => _CheckBox(); } class _CheckBox extends State<CheckBox> { @override Widget build(BuildContext context) { CityProvider cityProvider = context.read<CityProvider>(); final isChecked = context.select((CityProvider cityProvider) => cityProvider.isChecked); print('_CheckBox has rendered.'); //レンダリングされたことをDebugコンソールに表示 return CheckboxListTile( title: const Text('チェックボックス'), value: cityProvider.isChecked, onChanged: (bool? value) { setState(() { cityProvider.changeIsChecked(value!); }); }, ); } }
デバッグコンソールを見ながらWidgetを操作してみると、必要最低限のWidgetの再ビルドのみ行われるようになっていますね。
まとめ
context.watch<T>()
:プロバイダーで管理しているすべての値およびメソッドを監視し、読み取る場合に使用する。context.read<T>()
:プロバイダーで管理している値の初期値またはメソッドのみを読み取る場合に使用する。context.select<T, R>(R cb(T value))
:プロバイダーで管理している一部の値のみ監視し、読み取る場合に使用する。
参考
以上